package com.xcity.tools.proto;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileFilter;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.xcity.tools.proto.ProtoEntry.Property;

import naga.x.net.impl.pomelo.PomeloPacket;
import naga.x.util.ClassUtil;
import naga.x.util.IOUtils;
import naga.x.util.StringUtils;

public class ProtobufUtils {
	
	static File protoFileDir = new File("D:/protoc-3.2.0-win32/__test__/input");
	static File protoJavaCodeDir = new File("D:/protoc-3.2.0-win32/__test__/output");
	static String protoClassPackage = "com.xcity.proto.gen";
	static String protoInterpreterPackage = "com.xcity.proto.interpreter";
	static Map<Class<?>, String> protoTypes;
	static List<ProtoEntry> protoEntrys = new LinkedList<ProtoEntry>();
	
	public static void main(String[] args) throws Exception {
		registerProtoTypes();
		createProtoFileBatch();
		createProtoJavaCodeBatch();
		createProtoInterpreterBatch();
		System.out.println("ok");
	}
	
	private static void registerProtoTypes() {
//		.proto Type	Java Type
//		double	double
//		float	float
//		int32	int
//		int64	long
//		uint32	int[1]
//		uint64	long[1]
//		sint32	int
//		sint64	long
//		fixed32	int[1]
//		fixed64	long[1]
//		sfixed32	int
//		sfixed64	long
//		bool	boolean
//		string	String
//		bytes	ByteString

//		the tag id can not use 19000 ~ 19999
//		repeated type arr = tag [packed=true];
		Map<Class<?>, String> types = new LinkedHashMap<Class<?>, String>();
		types.put(double.class, "double");
		types.put(Double.class, "double");
		types.put(float.class, "float");
		types.put(Float.class, "float");
		types.put(byte.class, "sint32");
		types.put(Byte.class, "sint32");
		types.put(short.class, "sint32");
		types.put(Short.class, "sint32");
		types.put(int.class, "sint32");
		types.put(Integer.class, "sint32");
		types.put(long.class, "sint64");
		types.put(Long.class, "sint64");
		types.put(boolean.class, "bool");
		types.put(Boolean.class, "bool");
		types.put(String.class, "string");
		types.put(char.class, "string");
		types.put(Character.class, "string");
//		types.put("com.google.protobuf.ByteString", "bytes");
		types.put(JSONObject.class, "map<string, string>");
		types.put(Map.class, "map<string, string>");
		if (SerializerFeature.isEnabled(JSON.DEFAULT_GENERATE_FEATURE, SerializerFeature.WriteEnumUsingToString)
				|| SerializerFeature.isEnabled(JSON.DEFAULT_GENERATE_FEATURE, SerializerFeature.WriteEnumUsingName)) {
			types.put(Enum.class, "string");
		} else {
			types.put(Enum.class, "sint32");
		}
		protoTypes = types;
	}
	
	private static void createProtoFileBatch() throws Exception {
		Set<Class<?>> classes = ClassUtil.scanPackage("com.xcity.pomelo");
		for (Class<?> cls : classes) {
			if (PomeloPacket.class.isAssignableFrom(cls)) {
				System.err.println("try create: " + cls);
				createProtoFile(cls);
			}
		}
	}
	
	/**
	 * 生成该类对应的proto文件
	 * @param classType
	 */
	private static void createProtoFile(Class<?> classType) throws Exception {
		FileWriter fw = new FileWriter(new File(protoFileDir, getProtoFileName(classType)));
		ProtoEntry protoEntry = new ProtoEntry(fw.file(), classType);
		protoEntrys.add(protoEntry);
		Set<String> imports = new LinkedHashSet<String>();
		Map<String, String> fieldsMap = mappingFields(classType, imports, protoEntry);
		// syntax v2/v3
		fw.writeLine("syntax = \"proto3\";").newLine();
		// package
		fw.write("package ").write(protoClassPackage).writeLine(';').newLine();
		// imports
		for (String imp : imports) {
			fw.write("import \"").write(imp).writeLine("\";");
		}
		fw.newLine();
		// message begin
		fw.write("message ").write(getProtoClassName(classType)).writeLine(" {");
		// fields
		int tagId = 0;
		for (Map.Entry<String, String> e : fieldsMap.entrySet()) {
			// key: field name, value: proto type
			fw.write("\t").write(e.getValue()).write(' ').write(e.getKey()).write(" = ").write(++tagId).writeLine(';');
		}
		// message end
		fw.writeLine('}');
		
		fw.flush();
		System.err.println("create proto file ok: " + classType + "->" + fw.fileName());
	}
	
	private static Map<String, String> mappingFields(Class<?> classType, Set<String> imports, ProtoEntry protoEntry) throws Exception {
		Map<String, String> map = new LinkedHashMap<String, String>();
		BeanInfo bean = Introspector.getBeanInfo(classType, Object.class);
		PropertyDescriptor[] pds = bean.getPropertyDescriptors();
		for (PropertyDescriptor pd : pds) {
			Map.Entry<String, String> e = mappingField(classType, pd, imports);
			if (e != null) {
				map.put(e.getKey(), e.getValue());
				protoEntry.fields.add(new Property(pd));
			}
		}
		Field[] fields = classType.getFields();
		for (Field field : fields) {
			Map.Entry<String, String> e = mappingField(classType, field, imports);
			if (e != null) {
				map.put(e.getKey(), e.getValue());
				protoEntry.fields.add(new Property(field));
			}
		}
		return map;
	}
	
	private static Map.Entry<String, String> mappingField(Class<?> classType, PropertyDescriptor propertyDesc, Set<String> imports) throws Exception {
		Method getter = propertyDesc.getReadMethod();
		if (getter == null) {
			return null;
		}
		JSONField anno = getter.getAnnotation(JSONField.class);
		String name = propertyDesc.getName();
		if (anno == null) {
			try {
				Field f = getFieldDeeply(classType, name);
				anno = f.getAnnotation(JSONField.class);
			} catch (Exception e) {}
		}
		return mappingField(classType, propertyDesc.getPropertyType(), name, anno, imports);
	}
	
	private static Map.Entry<String, String> mappingField(Class<?> classType, Field field, Set<String> imports) throws Exception {
		if (/*Modifier.isFinal(field.getModifiers()) && */Modifier.isStatic(field.getModifiers())) {
			return null;
		}
		JSONField anno = field.getAnnotation(JSONField.class);
		return mappingField(classType, field.getType(), field.getName(), anno, imports);
	}
	
	/**
	 * 属性名、proto类型映射
	 * @param classType
	 * @param fieldType
	 * @param fieldName
	 * @param anno
	 * @param imports
	 * @return
	 * @throws Exception
	 */
	private static Map.Entry<String, String> mappingField(Class<?> classType, Class<?> fieldType, String fieldName, JSONField anno, Set<String> imports) throws Exception {
		if (anno != null) {
			if (!anno.serialize()) {
				return null;
			}
			if (StringUtils.isNotEmpty(anno.name())) {
				fieldName = anno.name();
			}
		}
		
		String prefix = "";
		if (fieldType.isArray()) {
			fieldType = fieldType.getComponentType();
			prefix = "repeated ";
		}
		String protoType = protoTypes.get(fieldType);
		if (protoType == null) {
			if (Map.class.isAssignableFrom(fieldType)) {
				protoType = protoTypes.get(Map.class);
			} else if (fieldType.isEnum()) {
				protoType = protoTypes.get(Enum.class);
			} else if (!fieldType.isInterface() && fieldType != Object.class && !fieldType.getName().startsWith("java.")) {
				createProtoFile(fieldType);
				protoType = protoClassPackage + "." + getProtoClassName(fieldType);
				imports.add(getProtoFileName(fieldType));
			} else {
				System.err.println("not found proto type: " + classType + "->" + fieldType + "=" + fieldName);
				return null;
			}
		}
		
		final String key = fieldName; final String value = prefix + protoType;
		return new Map.Entry<String, String>() {
			@Override
			public String setValue(String value) {
				return value;
			}
			@Override
			public String getValue() {
				return value;
			}
			@Override
			public String getKey() {
				return key;
			}
		};
	}
	
	/**
	 * 根据原生类获取对应的proto文件名
	 * @param classType
	 * @return
	 */
	static String getProtoFileName(Class<?> classType) {
		String name = getClassSimpleFullName(classType);
		StringBuilder builder = new StringBuilder();
		for (int i = 0; i < name.length(); i++) {
			char c = name.charAt(i);
			if (Character.isUpperCase(c)) {
				if (i > 0) {
					builder.append("_");
				}
				builder.append(Character.toLowerCase(c) + "");
			} else {
				builder.append(c + "");
			}
		}
		return builder.append("_factory.proto").toString();
	}
	
	/**
	 * 根据原生类获取对应的proto类名
	 * @param classType
	 * @return
	 */
	static String getProtoClassName(Class<?> classType) {
		String name = getClassSimpleFullName(classType);
		return name;
	}
	
	static String getProtoFactoryClassName(Class<?> classType) {
		return getProtoClassName(classType) + "Factory";
	}
	
	private static String getClassSimpleFullName(Class<?> classType) {
		String name = classType.getSimpleName();
		Class<?> enclosingClass = classType.getEnclosingClass();
		if (enclosingClass != null) {
			return getClassSimpleFullName(enclosingClass) + name;
		}
		return name;
	}
	
	private static void createProtoJavaCodeBatch() {
		protoFileDir.listFiles(new FileFilter() {
			@Override
			public boolean accept(File file) {
				try {
					createProtoJavaCode(file);
				} catch (Exception e) {
					e.printStackTrace();
				}
				return false;
			}
		});
	}
	
	private static void createProtoJavaCode(File protoFile) throws Exception {
		String[] cmds = {
				"protoc",
				"-I=" + protoFileDir.getAbsolutePath(),
				"--java_out=" + protoJavaCodeDir.getAbsolutePath(),
				protoFile.getAbsolutePath()
		};
		Process process = Runtime.getRuntime().exec(cmds);
		InputStream error = process.getErrorStream();
		String err = IOUtils.read(error, IOUtils.UTF8);
		error.close();
		if (StringUtils.isNotEmpty(err)) {
			System.err.println("create proto java code error: " + protoFile.getName());
			System.err.println(err);
		}
	}
	
	private static void createProtoInterpreterBatch() {
		for (ProtoEntry pe : protoEntrys) {
			pe.createInterpreterClass();
		}
	}
	
	private static Field getFieldDeeply(Class<?> classType, String name) throws Exception {
		Field f = null;
		try {
			f = classType.getDeclaredField(name);
		} catch (Exception e) {}
		if (f == null) {
			Class<?> cls = classType.getSuperclass();
			if (cls != null && cls != Object.class) {
				return getFieldDeeply(cls, name);
			}
		}
		return f;
	}
	
}
